﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace calendar
{
/**
* This class represents the a Calendar Definition record. Both base calendars
* and calendars derived from base calendars are represented by instances of
* this class. The class is used to define the working and non-working days of
* the week. The default calendar defines Monday to Friday as working days.
*/
public class ProjectCalendar : ProjectCalendarWeek
{
    private ProjectFile m_projectFile;

    private List<ProjectCalendarWeek> m_workWeeks = new List<ProjectCalendarWeek>();

    private List<ProjectCalendarException> m_exceptions = new List<ProjectCalendarException>();

    private bool m_exceptionsSorted = false;


    private Hashtable m_startTimeCache = new Hashtable();

    private Hashtable m_workingDateCache = new Hashtable();

    private List<ProjectCalendar> m_derivedCalendars = new List<ProjectCalendar>();

    /**
	 * Default constructor.
	 * 
	 * @param file
	 *            the parent file to which this record belongs.
	 */
    public ProjectCalendar(ProjectFile file)
    {
        m_projectFile = file;

        if (file.getAutoCalendarUniqueID() == true)
        {
            //setUniqueID(Integer.valueOf(file.getCalendarUniqueID()));
        }
    }

    public ProjectCalendarWeek addWorkWeek()
    {
        ProjectCalendarWeek pcw = new ProjectCalendarWeek();
        pcw.Parent = this;
        m_workWeeks.Add(pcw);
        return pcw;
    }

    public List<ProjectCalendarWeek> getWorkWeeks()
    {
        return m_workWeeks;
    }

    public ProjectCalendarException addCalendarException(DateTime start, DateTime end)
    {
        ProjectCalendarException pce = new ProjectCalendarException(start, end);
        m_exceptions.Add(pce);
        m_exceptionsSorted = false;
        clearWorkingDateCache();
        return (pce);
    }

    public ProjectFile ParentFile
    {
        get
        {
            return m_projectFile;
        }
    }

    /**
	 * Utility method to clear cached calendar data.
	 */
    private void clearWorkingDateCache() {
		m_workingDateCache.Clear();
		m_startTimeCache.Clear();
		foreach (ProjectCalendar calendar in m_derivedCalendars) {
			calendar.clearWorkingDateCache();
		}
	}

    public List<ProjectCalendarException> getCalnedarExceptions() {
        if (!m_exceptionsSorted)
        {
            m_exceptions.Sort();
        }
        return (m_exceptions);    
    }

    /**
	 * Used to add working hours to the calendar. Note that the MPX file
	 * definition allows a maximum of 7 calendar hours records to be added to a
	 * single calendar.
	 * 
	 * @param day
	 *            day number
	 * @return new ProjectCalendarHours instance
	 */
	
	public override ProjectCalendarHours addCalendarHours(Day day) {
		clearWorkingDateCache();        
		return base.addCalendarHours(day);
	}

    /**
	 * Attaches a pre-existing set of hours to the correct day within the
	 * calendar.
	 * 
	 * @param hours
	 *            calendar hours instance
	 */
	
	public override void attachHoursToDay(ProjectCalendarHours hours) {
		clearWorkingDateCache();
		base.attachHoursToDay(hours);
	}

    /**
	 * Removes a set of calendar hours from the day to which they are currently
	 * attached.
	 * 
	 * @param hours
	 *            calendar hours instance
	 */
	
	public override void removeHoursFromDay(ProjectCalendarHours hours) {
		clearWorkingDateCache();
		base.removeHoursFromDay(hours);
	}

    /**
	 * Sets the ProjectCalendar instance from which this calendar is derived.
	 * 
	 * @param calendar
	 *            base calendar instance
	 */
   	public  ProjectCalendar Parent {
        set 
        {
            if (this.Parent != null)
            {
                this.Parent.removeDerivedCalendar(this);
            }

            base.Parent = (ProjectCalendar)value;

            if ((ProjectCalendar)value != null)
            {
                ((ProjectCalendar)value).addDerivedCalendar(this);
            }
            clearWorkingDateCache();
        }
        get
        {
            return (ProjectCalendar)base.Parent;
        }
	}

    protected void removeDerivedCalendar(ProjectCalendar calendar)
    {
        m_derivedCalendars.Remove(calendar);
    }

    protected void addDerivedCalendar(ProjectCalendar calendar)
    {
        m_derivedCalendars.Add(calendar);
    }

    /**
	 * Method indicating whether a day is a working or non-working day.
	 * 
	 * @param day
	 *            required day
	 * @return true if this is a working day
	 */
    public bool isWorkingDay(Day day)
    {
        DayType value = getWorkingDay(day);
        bool result;

        if (value.getValue() == DayType.DEFAULT)
        {
            ProjectCalendar cal = this.Parent;
            if (cal != null)
            {
                result = cal.isWorkingDay(day);
            }
            else
            {
                result = (day != Day.SATURDAY && day != Day.SUNDAY);
            }
        }
        else
        {
            result = (value.getValue() == DayType.WORKING);
        }

        return (result);
    }

    public Duration getDuration(DateTime startDate, DateTime endDate)
    {
        int dayIndex = startDate.DayOfYear;
        int days = getDaysInRange(startDate, endDate);
        int duration = 0;
        DateTime iter = startDate;
        while (days > 0) 
        {
            if (isWorkingDate(iter, Day.getInstance(dayIndex)) == true)
            {
                ++duration;
            }
            --days;
            ++dayIndex;
            if (dayIndex > 7) 
            {
                dayIndex = 1;
            }
            iter.AddDays(1);
        }
        return (Duration.getInstance(duration, TimeUnit.DAYS));
    }

    private int getDaysInRange(DateTime startDate, DateTime endDate)
    {
        int result = 0;
        DateTime iter = startDate;
        if (startDate.Year == endDate.Year) //年份相同
        {
            result = endDate.DayOfYear - startDate.DayOfYear + 1;
        }
        else
        {
            do
            {
                DateTime lastDay = new DateTime(iter.Year, 12, 31);
                result += (lastDay.DayOfYear - iter.DayOfYear + 1);
                iter = new DateTime(iter.Year + 1, 1, 1, iter.Hour, iter.Minute, iter.Second);
            } while (iter.Year < endDate.Year);
            result += endDate.DayOfYear;
        }
        return result;
    }

    /**
	 * This method allows the caller to determine if a given date is a working
	 * day. This method takes account of calendar exceptions.
	 * 
	 * @param date
	 *            Date to be tested
	 * @return boolean value
	 */
    public bool isWorkingDate(DateTime date)
    {
        Day day = Day.getInstance((int)date.DayOfWeek);
        return (isWorkingDate(date, day));        
    }

    /**
     * This private method allows the caller to determine if a given date is a
     * working day. This method takes account of calendar exceptions. It assumes
     * that the caller has already calculated the day of the week on which the
     * given day falls.
     * 
     * @param date
     *            Date to be tested
     * @param day
     *            Day of the week for the date under test
     * @return boolean flag
     */
    private bool isWorkingDate(DateTime date, Day day)
    {        
        ProjectCalendarDateRange ranges = getRanges(date, day);        
        return ranges.getRangeCount() != 0;
    }

    /**
	 * Retrieves the working hours on the given date.
	 * 
	 * @param date
	 *            required date
	 * @param cal
	 *            optional calendar instance
	 * @param day
	 *            optional day instance
	 * @return working hours
	 */
    private ProjectCalendarDateRange getRanges(DateTime date, Day day)
    {
        ProjectCalendarDateRange ranges = getException(date);
        if (ranges == null)
        {
            if (day == null)
            {
                day = Day.getInstance((int)date.DayOfWeek);
            }
            ranges = getHours(day);
        }
        return ranges;        
    }

    /**
	 * Retrieve a calendar calendar exception which applies to this date.
	 * 
	 * @param date
	 *            target date
	 * @return calendar exception, or null if none match this date
	 */
    public ProjectCalendarException getException(DateTime date) {
		ProjectCalendarException exception = null;
		if (m_exceptions.Count > 0) {
			if (!m_exceptionsSorted) {
				m_exceptions.Sort();
				m_exceptionsSorted = true;
			}

			int low = 0;
			int high = m_exceptions.Count - 1;
			long targetDate = date.Ticks;

			while (low <= high) {
				int mid = (low + high) / 2;
				ProjectCalendarException midVal = m_exceptions[mid];
				int cmp = 0 - DateUtility.compare(midVal.FromDate,
						midVal.ToDate, targetDate);

				if (cmp < 0) {
					low = mid + 1;
				} else {
					if (cmp > 0) {
						high = mid - 1;
					} else {
						exception = midVal;
						break;
					}
				}
			}
		}

		if (exception == null && Parent != null) {
			// Check base calendar as well for an exception.
			exception = Parent.getException(date);
		}
		return (exception);
	}

    public DateTime getStartTime(DateTime date)
    {        
        DateTime result = (DateTime)m_startTimeCache[date];
        if (result == null)
        {
            ProjectCalendarDateRange ranges = getRanges(date, null);
            if (ranges == null)
            {
                result = ParentFile.getProjectHeader().getDefaultStartTime();
            }
            else
            {
                result = ranges.getRange(0).getStart();
            }
            result = DateUtility.getCanonicalTime(result);
            m_startTimeCache.Add(date, result);
        }

        return result;
    }
}
}
